package com.easyaop.api.around.annotation;

import net.bytebuddy.asm.Advice;
import net.bytebuddy.build.HashCodeAndEqualsPlugin;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.method.ParameterDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.bind.MethodDelegationBinder;
import net.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder;
import net.bytebuddy.implementation.bytecode.StackManipulation;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.implementation.bytecode.constant.ClassConstant;
import net.bytebuddy.implementation.bytecode.constant.IntegerConstant;
import net.bytebuddy.implementation.bytecode.constant.MethodConstant;
import net.bytebuddy.implementation.bytecode.constant.TextConstant;
import net.bytebuddy.implementation.bytecode.member.MethodInvocation;
import net.bytebuddy.utility.JavaConstant;
import net.bytebuddy.utility.JavaType;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.easyaop.api.around.CustomOrigin.DEFAULT;
import static net.bytebuddy.matcher.ElementMatchers.named;

import com.easyaop.api.around.CustomOrigin;

/**
 * @author lipan
 * @since 2025-02-22
 */
public @interface CustomOriginImpl {


    enum Binder implements TargetMethodAnnotationDrivenBinder.ParameterBinder<CustomOrigin> {
        INSTANCE;

        private static final MethodDescription.InDefinedShape CACHE;

        private static final MethodDescription.InDefinedShape PRIVILEGED;

        private static final MethodDescription.InDefinedShape ORIGIN_VALUE;

        static {
            MethodList<MethodDescription.InDefinedShape> methods =
                    TypeDescription.ForLoadedType.of(CustomOrigin.class).getDeclaredMethods();
            CACHE = methods.filter(named("cache")).getOnly();
            PRIVILEGED = methods.filter(named("privileged")).getOnly();
            ORIGIN_VALUE = TypeDescription.ForLoadedType.of(CustomOrigin.class)
                    .getDeclaredMethods()
                    .filter(named("value"))
                    .getOnly();
        }

        /**
         * Loads a method constant onto the operand stack.
         *
         * @param annotation        The origin annotation.
         * @param methodDescription The method description to load.
         * @return An appropriate stack manipulation.
         */
        private static StackManipulation methodConstant(AnnotationDescription.Loadable<CustomOrigin> annotation,
                                                        MethodDescription.InDefinedShape methodDescription) {
            MethodConstant.CanCache methodConstant = annotation.getValue(PRIVILEGED).resolve(Boolean.class)
                    ? MethodConstant.ofPrivileged(methodDescription)
                    : MethodConstant.of(methodDescription);
            return annotation.getValue(CACHE).resolve(Boolean.class)
                    ? methodConstant.cached()
                    : methodConstant;
        }

        private static StackManipulation textConstant(AnnotationDescription.Loadable<CustomOrigin> annotation,
                                                      MethodDescription.InDefinedShape methodDescription) {

            String text = ForOrigin.parse(annotation.getValue(ORIGIN_VALUE).resolve(String.class))
                    .resolve(methodDescription);
            return new TextConstant(text);
        }

        /**
         * {@inheritDoc}
         */
        public Class<CustomOrigin> getHandledType() {
            return CustomOrigin.class;
        }

        /**
         * {@inheritDoc}
         */
        public MethodDelegationBinder.ParameterBinding<?> bind(AnnotationDescription.Loadable<CustomOrigin> annotation,
                                                               MethodDescription source,
                                                               ParameterDescription target,
                                                               Implementation.Target implementationTarget,
                                                               Assigner assigner,
                                                               Assigner.Typing typing) {
            TypeDescription parameterType = target.getType().asErasure();
            if (parameterType.represents(Class.class)) {
                return new MethodDelegationBinder.ParameterBinding.Anonymous(ClassConstant.of(implementationTarget.getOriginType().asErasure()));
            } else if (parameterType.represents(Method.class)) {
                return source.isMethod()
                        ? new MethodDelegationBinder.ParameterBinding.Anonymous(methodConstant(annotation,
                        source.asDefined()))
                        : MethodDelegationBinder.ParameterBinding.Illegal.INSTANCE;
            } else if (parameterType.represents(Constructor.class)) {
                return source.isConstructor()
                        ? new MethodDelegationBinder.ParameterBinding.Anonymous(methodConstant(annotation,
                        source.asDefined()))
                        : MethodDelegationBinder.ParameterBinding.Illegal.INSTANCE;
            } else if (JavaType.EXECUTABLE.getTypeStub().equals(parameterType)) {
                return new MethodDelegationBinder.ParameterBinding.Anonymous(methodConstant(annotation,
                        source.asDefined()));
            } else if (parameterType.represents(String.class)) {
                return new MethodDelegationBinder.ParameterBinding.Anonymous(textConstant(annotation,
                        source.asDefined()));
            } else if (parameterType.represents(int.class)) {
                return new MethodDelegationBinder.ParameterBinding.Anonymous(IntegerConstant.forValue(source.getModifiers()));
            } else if (parameterType.equals(JavaType.METHOD_HANDLE.getTypeStub())) {
                return new MethodDelegationBinder.ParameterBinding.Anonymous(JavaConstant.MethodHandle.of(source.asDefined()).toStackManipulation());
            } else if (parameterType.equals(JavaType.METHOD_TYPE.getTypeStub())) {
                return new MethodDelegationBinder.ParameterBinding.Anonymous(JavaConstant.MethodType.of(source.asDefined()).toStackManipulation());
            } else if (parameterType.equals(JavaType.METHOD_HANDLES_LOOKUP.getTypeStub())) {
                return new MethodDelegationBinder.ParameterBinding.Anonymous(MethodInvocation.lookup());
            } else {
                throw new IllegalStateException("The " + target + " method's " + target.getIndex() +
                        " parameter is annotated with a Origin annotation with an argument not representing a Class," +
                        " Method, Constructor, String, int, MethodType or MethodHandle type");
            }
        }
    }

    class ForOrigin {

        /**
         * The delimiter character.
         */
        private static final char DELIMITER = '#';

        /**
         * The escape character.
         */
        private static final char ESCAPE = '\\';

        /**
         * The renderers to apply.
         */
        private final List<Renderer> renderers;

        /**
         * Creates a new offset mapping for an origin value.
         *
         * @param renderers The renderers to apply.
         */
        public ForOrigin(List<Renderer> renderers) {
            this.renderers = renderers;
        }

        /**
         * Parses a pattern of an origin annotation.
         *
         * @param pattern The supplied pattern.
         * @return An appropriate offset mapping.
         */
        public static ForOrigin parse(String pattern) {
            if (pattern.equals(DEFAULT)) {
                return new ForOrigin(Collections.singletonList(Renderer.ForStringRepresentation.INSTANCE));
            } else {
                List<Renderer> renderers = new ArrayList<>(pattern.length());
                int from = 0;
                for (int to = pattern.indexOf(DELIMITER); to != -1; to = pattern.indexOf(DELIMITER, from)) {
                    if (to != 0 && pattern.charAt(to - 1) == ESCAPE && (to == 1 || pattern.charAt(to - 2) != ESCAPE)) {
                        renderers.add(new Renderer.ForConstantValue(pattern.substring(from, Math.max(0,
                                to - 1)) + DELIMITER));
                        from = to + 1;
                        continue;
                    } else if (pattern.length() == to + 1) {
                        throw new IllegalStateException("Missing sort descriptor for " + pattern + " at index " + to);
                    }
                    renderers.add(new Renderer.ForConstantValue(pattern.substring(from, to).replace("" + ESCAPE + ESCAPE, "" + ESCAPE)));
                    switch (pattern.charAt(to + 1)) {
                        case Renderer.ForMethodName.SYMBOL:
                            renderers.add(Renderer.ForMethodName.INSTANCE);
                            break;
                        case Renderer.ForTypeName.SYMBOL:
                            renderers.add(Renderer.ForTypeName.INSTANCE);
                            break;
                        case Renderer.ForDescriptor.SYMBOL:
                            renderers.add(Renderer.ForDescriptor.INSTANCE);
                            break;
                        case Renderer.ForReturnTypeName.SYMBOL:
                            renderers.add(Renderer.ForReturnTypeName.INSTANCE);
                            break;
                        case Renderer.ForJavaSignature.SYMBOL:
                            renderers.add(Renderer.ForJavaSignature.INSTANCE);
                            break;
                        case Renderer.ForPropertyName.SYMBOL:
                            renderers.add(Renderer.ForPropertyName.INSTANCE);
                            break;
                        default:
                            throw new IllegalStateException("Illegal sort descriptor " + pattern.charAt(to + 1) + " " +
                                    "for " + pattern);
                    }
                    from = to + 2;
                }
                renderers.add(new Renderer.ForConstantValue(pattern.substring(from)));
                return new ForOrigin(renderers);
            }
        }

        /**
         * {@inheritDoc}
         */
        public String resolve(MethodDescription instrumentedMethod) {
            StringBuilder stringBuilder = new StringBuilder();
            for (Renderer renderer : renderers) {
                stringBuilder.append(renderer.apply(instrumentedMethod.getDeclaringType().asErasure(),
                        instrumentedMethod));
            }
            return stringBuilder.toString();
        }

        /**
         * A renderer for an origin pattern element.
         */
        public interface Renderer {

            /**
             * Returns a string representation for this renderer.
             *
             * @param instrumentedType   The instrumented type.
             * @param instrumentedMethod The instrumented method.
             * @return The string representation.
             */
            String apply(TypeDescription instrumentedType, MethodDescription instrumentedMethod);

            /**
             * A renderer for a method's internal name.
             */
            enum ForMethodName implements Renderer {

                /**
                 * The singleton instance.
                 */
                INSTANCE;

                /**
                 * The method name symbol.
                 */
                public static final char SYMBOL = 'm';

                /**
                 * {@inheritDoc}
                 */
                public String apply(TypeDescription instrumentedType, MethodDescription instrumentedMethod) {
                    return instrumentedMethod.getInternalName();
                }
            }

            /**
             * A renderer for a method declaring type's binary name.
             */
            enum ForTypeName implements Renderer {

                /**
                 * The singleton instance.
                 */
                INSTANCE;

                /**
                 * The type name symbol.
                 */
                public static final char SYMBOL = 't';

                /**
                 * {@inheritDoc}
                 */
                public String apply(TypeDescription instrumentedType, MethodDescription instrumentedMethod) {
                    return instrumentedType.getName();
                }
            }

            /**
             * A renderer for a method descriptor.
             */
            enum ForDescriptor implements Renderer {

                /**
                 * The singleton instance.
                 */
                INSTANCE;

                /**
                 * The descriptor symbol.
                 */
                public static final char SYMBOL = 'd';

                /**
                 * {@inheritDoc}
                 */
                public String apply(TypeDescription instrumentedType, MethodDescription instrumentedMethod) {
                    return instrumentedMethod.getDescriptor();
                }
            }

            /**
             * A renderer for a method's Java signature in binary form.
             */
            enum ForJavaSignature implements Renderer {

                /**
                 * The singleton instance.
                 */
                INSTANCE;

                /**
                 * The signature symbol.
                 */
                public static final char SYMBOL = 's';

                /**
                 * {@inheritDoc}
                 */
                public String apply(TypeDescription instrumentedType, MethodDescription instrumentedMethod) {
                    StringBuilder stringBuilder = new StringBuilder("(");
                    boolean comma = false;
                    for (TypeDescription typeDescription :
                            instrumentedMethod.getParameters().asTypeList().asErasures()) {
                        if (comma) {
                            stringBuilder.append(',');
                        } else {
                            comma = true;
                        }
                        stringBuilder.append(typeDescription.getName());
                    }
                    return stringBuilder.append(')').toString();
                }
            }

            /**
             * A renderer for a method's return type in binary form.
             */
            enum ForReturnTypeName implements Renderer {

                /**
                 * The singleton instance.
                 */
                INSTANCE;

                /**
                 * The return type symbol.
                 */
                public static final char SYMBOL = 'r';

                /**
                 * {@inheritDoc}
                 */
                public String apply(TypeDescription instrumentedType, MethodDescription instrumentedMethod) {
                    return instrumentedMethod.getReturnType().asErasure().getName();
                }
            }

            /**
             * A renderer for a method's {@link Object#toString()} representation.
             */
            enum ForStringRepresentation implements Renderer {

                /**
                 * The singleton instance.
                 */
                INSTANCE;

                /**
                 * {@inheritDoc}
                 */
                public String apply(TypeDescription instrumentedType, MethodDescription instrumentedMethod) {
                    return instrumentedMethod.toString();
                }
            }

            /**
             * A renderer for a constant value.
             */
            @HashCodeAndEqualsPlugin.Enhance
            class ForConstantValue implements Renderer {

                /**
                 * The constant value.
                 */
                private final String value;

                /**
                 * Creates a new renderer for a constant value.
                 *
                 * @param value The constant value.
                 */
                public ForConstantValue(String value) {
                    this.value = value;
                }

                /**
                 * {@inheritDoc}
                 */
                public String apply(TypeDescription instrumentedType, MethodDescription instrumentedMethod) {
                    return value;
                }
            }

            /**
             * A renderer for a property name.
             */
            enum ForPropertyName implements Renderer {

                /**
                 * The singleton instance.
                 */
                INSTANCE;

                /**
                 * The signature symbol.
                 */
                public static final char SYMBOL = 'p';

                /**
                 * {@inheritDoc}
                 */
                public String apply(TypeDescription instrumentedType, MethodDescription instrumentedMethod) {
                    return FieldAccessor.FieldNameExtractor.ForBeanProperty.INSTANCE.resolve(instrumentedMethod);
                }
            }
        }
    }
}
